package ebhack;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.ListIterator;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

public class SpriteSetsEditor extends ToolModule implements ActionListener {
	public static final int NUM_SPRITE_SETS = 463;
	public static final int NUM_PALS = 8;
	
	private static final ImageIcon blankIcon = new ImageIcon(new BufferedImage(1, 1,BufferedImage.TYPE_4BYTE_ABGR));
	
	private static Sprite.Palette[] pals;
	private static SpriteSet[] sets;
	private static Hashtable<String, Sprite> sprites;
	private static int numSprites;
	
	private static String[] spsetNames = null;
	
	private JButton[] pics;
	private JTextField name, width, height;
	private JComboBox palette, selector, numSpritesBox;
	private JTextField[] spriteFields;
	private JCheckBox[] flipBoxes;
	private JButton preview;
	
	public SpriteSetsEditor(XMLPreferences prefs) {
		super(prefs);
		
		reset();
	}
	
	public void reset() {
		pals = new Sprite.Palette[NUM_PALS];
		sets = new SpriteSet[NUM_SPRITE_SETS];
		sprites = new Hashtable<String, Sprite>();
		
		initSetNames();
	}
	
    public static void initSetNames()
    {
    	if (spsetNames == null) {
    		spsetNames = new String[SpriteSetsEditor.NUM_SPRITE_SETS];
            // TODO: allow custom sptName.txt names
            readArray(SpriteSetsEditor.class.getClassLoader(), DEFAULT_DATA_DIR,
                "sptNames.txt", false, spsetNames);
    	}
    }
    
    public static int getNumSprites() {
    	return numSprites;
    }
    
    public static Sprite newSprite() {
    	String id = Integer.toString(numSprites);
    	Sprite sp = new Sprite(2, 3, id);
    	sprites.put(id, sp);
    	numSprites++;
    	return sp;
    }
    
    public static void deleteSprite(Sprite sp) {
    	sprites.remove(sp.getId());
    	if (numSprites > 1) {
    		// Change the last sprite's Id to the removed sprite's Id
        	Sprite sp2 = sprites.get(Integer.toString(numSprites-1));
        	sprites.remove(sp2.getId());
        	sp2.setId(sp.getId());
        	sprites.put(sp2.getId(), sp2);
    	}
    	numSprites--;
    }
    
    public static Sprite getSprite(String id) {
    	return sprites.get(id);
    }
    
    public static Sprite.Palette getPalette(int i) {
    	return pals[i];
    }
    
    public static SpriteSet getSpriteSet(Sprite sp) {
    	for (int i=0; i<NUM_SPRITE_SETS; i++) {
    		for (int j=0; j<sets[i].getNumSprites(); j++) {
    			if (sets[i].getSprite(j).equals(sp)) {
    				return sets[i];
    			}
    		}
    	}
    	return null;
    }

	public String getDescription() {
		return "Sprite Sets Editor";
	}

	public String getVersion() {
		return "0.1";
	}

	public String getCredits() {
		return "Written by MrTenda";
	}

	public void init() {
        mainWindow = createBaseWindow(this);
        mainWindow.setTitle(this.getDescription());
        
        pics = new JButton[16];
        JPanel display = new JPanel(new GridLayout(4, 4));
        for (int i = 0; i < pics.length; i++)
        {
            display.add(pics[i] = new JButton(Integer.toString(i)));
            pics[i].setBorder(null);
            pics[i].setActionCommand("pics" + Integer.toHexString(i));
            pics[i].addActionListener(this);
        }
        mainWindow.getContentPane().add(display, BorderLayout.CENTER);
        
        Box entry = new Box(BoxLayout.Y_AXIS);
        //entry.add(getLabeledComponent("Name: ", name = new JTextField(20)));
        entry.add(getLabeledComponent("Width: ", width = createSizedJTextField(3, true)));
        entry.add(getLabeledComponent("Height: ", height = createSizedJTextField(3, true)));
        entry.add(getLabeledComponent("Palette: ",
            palette = createJComboBoxFromArray(new Object[8])));
        entry.add(getLabeledComponent("Sprites: ",
        		numSpritesBox = createJComboBoxFromArray(new Object[17])));
        numSpritesBox.addActionListener(this);
        palette.setActionCommand("pal");
        palette.addActionListener(this);
        spriteFields = new JTextField[16];
        flipBoxes = new JCheckBox[16];
        for (int i = 0; i < spriteFields.length; i++)
            entry.add(getLabeledComponent("Sprite #" + i + ": ",
                ToolModule.pairComponents(
                		flipBoxes[i] = new JCheckBox(),
                		spriteFields[i] = createSizedJTextField(4, true, true),
                		true)));
        preview = new JButton("Preview");
        preview.addActionListener(this);
        entry.add(preview);
        mainWindow.getContentPane().add(entry, BorderLayout.EAST);
        
        mainWindow.getContentPane().add(
                new JSearchableComboBox(selector = createComboBox(spsetNames, true,
                    this), "Entry #:"), BorderLayout.NORTH);
        
        mainWindow.invalidate();
        mainWindow.pack();
        //mainWindow.setSize(300, 400);
        mainWindow.setLocationByPlatform(true);
        mainWindow.validate();
        mainWindow.setResizable(false);
	}
	
	public static class SpriteSet {
		private Sprite[] sprites = new Sprite[16];
		private boolean[] flips = new boolean[16];
		private int[] unknowns = new int[5];
		private int height, width, palette, numSprites;
		
		public SpriteSet(int width, int height, int palette) {
			this.width = width;
			this.height = height;
			this.palette = palette;
			
			numSprites = 0;
			
			for (int i = 0; i<sprites.length; i++)
				sprites[i] = null;
			
			for (int i = 0; i<flips.length; i++)
				flips[i] = false;
			
			for (int i = 0; i<unknowns.length; i++)
				unknowns[i] = 0;
		}
		
		public void setUnknown(int i, int val) {
			unknowns[i] = val;
		}
		
		public int getUnknown(int i) {
			return unknowns[i];
		}
		
		public int getWidth() {
			return width;
		}
		
		public void setWidth(int width) {
			this.width = width;
		}
		
		public int getHeight() {
			return height;
		}
		
		public void setHeight(int height) {
			this.height = height;
		}
		
		public int getPalette() {
			return palette;
		}
		
		public void setPalette(int palette) {
			this.palette = palette;
		}
		
		public void setSprite(int i, Sprite sprite, boolean flip) {
			sprites[i] = sprite;
			flips[i] = flip;
		}
		
		public boolean isFlipped(int i) {
			return flips[i];
		}
		
		public void setNumSprites(int numSprites) {
			this.numSprites = numSprites;
		}
		
		public int getNumSprites() {
			return numSprites;
		}
		
		public Sprite getSprite(int i) {
			return sprites[i];
		}
	}
	
	public static class Sprite {
		private String key;
		private int width, height;
		private int[][] image;
		
		public Sprite(int width, int height, String key) {
			this.width = width;
			this.height = height;
			this.key = key;
			
			image = new int[width*8][height*8];
			
			for (int i = 0; i < height*8; i++)
				for (int j=0; j<width*8; j++)
					image[j][i] = 0;
		}
		
		public Sprite(int width, int height, String key, String s) {
			this.width = width;
			this.height = height;
			this.key = key;
			
			image = new int[width*8][height*8];
			setFromString(s);
		}
		
		public void setFromString(String s) {
			int pos = 0;
			for (int i=0; i<height*8; i++) {
				for (int j=0; j<width*8; j++) {
					image[j][i] = Integer.parseInt(s.substring(pos,pos+1), 16);
					pos++;
				}
			}
		}
		
		public String getAsString() {
			String out = new String();
			for (int i=0; i<height*8; i++) {
				for (int j=0; j<width*8; j++) {
					out += Integer.toString(image[j][i], 16);
				}
			}
			return out;
		}
		
		public String getId() {
			return key;
		}
		
		public void setId(String id) {
			this.key = id;
		}
		
		public int[][] getImage() {
			return image;
		}
		
        public void setImage(int[][] in)
        {
            for (int x = 0; x < image.length; x++)
            {
                for (int y = 0; y < image[0].length; y++)
                {
                    image[x][y] = in[x][y];
                }
            }
        }
		
		public int getWidth() {
			return width;
		}
		
		public int getHeight() {
			return height;
		}
		
		public void resize(int width, int height) {
			this.width = width;
			this.height = height;
			image = new int[width*8][height*8];
		}
		
		public static class Palette {
			private Color[] colors; // RGB
			
			public Palette(String s) {
				colors = new Color[16];
				
				setFromString(s);
			}
			
			public void setFromString(String s) {
				for (int i=0; i<colors.length; i++) {
					colors[i] = new Color(
							Integer.parseInt(s.substring(i * 3, i * 3 + 1), 32) << 3,
							Integer.parseInt(s.substring(i * 3 + 1, i * 3 + 2), 32) << 3,
							Integer.parseInt(s.substring(i * 3 + 2, i * 3 + 3), 32) << 3);
				}
			}
			
			public void setColors(Color[] cs) {
				for (int i = 0; i < colors.length; i++) {
					colors[i] = new Color(cs[i].getRed(), cs[i].getGreen(), cs[i].getBlue());
				}
			}
			
			public void setColor(int cnum, Color c) {
				colors[cnum] = new Color(c.getRed(), c.getGreen(), c.getBlue());
			}
			
			public String getAsString() {
				String out = new String();
				
				for (int i=0; i<colors.length; i++) {
					out += Integer.toString(colors[i].getRed() >> 3, 32);
					out += Integer.toString(colors[i].getGreen() >> 3, 32);
					out += Integer.toString(colors[i].getBlue() >> 3, 32);
				}
				
				return out;
			}
			
			public Color[] getColorPalette() {
				return colors;
			}
		}
	}

	public void load(Project proj) {
		File f = new File(proj.getFilename("sprites"));
		try {
			Document dom = new SAXBuilder().build(f);
			
			ListIterator<Element> li, li2;
			Element e, sube;
			int i, j;
			
			// Set Palettes
			li = dom.getRootElement().getChild("Palettes").getChildren("Palette").listIterator();
			while (li.hasNext()) {
				e = li.next();
				i = ToolModule.parseUserInt(e.getAttributeValue("id"));
				pals[i] = new Sprite.Palette(e.getText());
			}
			
			// Set Sprites
			li = dom.getRootElement().getChild("Sprites").getChildren("Sprite").listIterator();
			Sprite sp;
			numSprites = 0;
			while (li.hasNext()) {
				e = li.next();
				sp = new Sprite(
						ToolModule.parseUserInt(e.getAttributeValue("width")),
						ToolModule.parseUserInt(e.getAttributeValue("height")),
						e.getAttributeValue("id"),
						e.getText());
				sprites.put(e.getAttributeValue("id"), sp);
				numSprites++;
			}
			
			// Set SpriteSets
			li = dom.getRootElement().getChild("SpriteSets").getChildren("SpriteSet").listIterator();
			while (li.hasNext()) {
				e = li.next();
				i = ToolModule.parseUserInt(e.getAttributeValue("id"))-1;
				sets[i] = new SpriteSet(
						ToolModule.parseUserInt(e.getChildText("Width")),
						ToolModule.parseUserInt(e.getChildText("Height")),
						ToolModule.parseUserInt(e.getChildText("Palette")));
				
				li2 = e.getChild("Unknowns").getChildren("Unknown").listIterator();
				for (j=0; j<5; j++) {
					sets[i].setUnknown(j, ToolModule.parseUserInt(li2.next().getText()));
				}
				
				li2 = e.getChild("Sprites").getChildren("Sprite").listIterator();
				j=0;
				while (li2.hasNext()) {
					sube = li2.next();
					sets[i].setSprite(j++,
							sprites.get(sube.getText()),
							sube.getAttributeValue("flip").equals("1"));
				}
				sets[i].setNumSprites(j);
			}
		} catch (JDOMException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void save(Project proj) {
		File f = new File(proj.getFilename("sprites"));
		
    	Element root = new Element("Sprites");
    	Document dom = new Document(root);
    	
    	Element section, entry, field, subfield;
    	
    	// Write palettes
    	section = new Element("Palettes");
    	root.addContent(section);
    	for (int i = 0; i < NUM_PALS; i++) {
    		entry = new Element("Palette");
    		section.addContent(entry);
    		entry.setAttribute("id", Integer.toString(i));
    		entry.setText(pals[i].getAsString());
    	}
    	
    	// Write SpriteSets
    	section = new Element("SpriteSets");
    	root.addContent(section);
    	for (int i = 0; i < NUM_SPRITE_SETS; i++) {
    		entry = new Element("SpriteSet");
    		section.addContent(entry);
    		entry.setAttribute("id", Integer.toString(i+1));
    		
    		field = new Element("Width");
    		entry.addContent(field);
    		field.setText(Integer.toString(sets[i].getWidth()));
    		field = new Element("Height");
    		entry.addContent(field);
    		field.setText(Integer.toString(sets[i].getHeight()));
    		field = new Element("Palette");
    		entry.addContent(field);
    		field.setText(Integer.toString(sets[i].getPalette()));
    		
    		field = new Element("Sprites");
    		entry.addContent(field);
    		for (int j = 0; j < sets[i].getNumSprites(); j++) {
    			subfield = new Element("Sprite");
    			field.addContent(subfield);
    			subfield.setAttribute("flip", sets[i].isFlipped(j) ? "1" : "0");
    			subfield.setText(sets[i].getSprite(j).getId());
    		}
    		
    		field = new Element("Unknowns");
    		entry.addContent(field);
    		for (int j = 0; j < 5; j++) {
    			subfield = new Element("Unknown");
    			field.addContent(subfield);
    			subfield.setText(ToolModule.toUserString(sets[i].getUnknown(j), 16));
    		}
    	}
    	
    	// Write Sprites
    	section = new Element("Sprites");
    	root.addContent(section);
    	for (int i = 0; i < numSprites; i++) {
    		Sprite sp = sprites.get(Integer.toString(i));
    		entry = new Element("Sprite");
    		section.addContent(entry);
    		entry.setAttribute("id", sp.getId());
    		entry.setAttribute("width", Integer.toString(sp.getWidth()));
    		entry.setAttribute("height", Integer.toString(sp.getHeight()));
    		entry.setText(sp.getAsString());
    	}
    	
        try {
        	FileOutputStream fos = new FileOutputStream(f);
            new XMLOutputter(Format.getPrettyFormat()).output(dom.getDocument(), fos);
        } catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void show() {
		super.show();
		selector.setSelectedIndex(Math.max(selector.getSelectedIndex(), 0));
		mainWindow.setVisible(true);
	}
	
	public void show(Object in) {
		super.show(in);
		if (in instanceof Sprite) {
			Sprite sp = (Sprite) in;
	    	for (int i=0; i<NUM_SPRITE_SETS; i++) {
	    		for (int j=0; j<sets[i].getNumSprites(); j++) {
	    			if (sets[i].getSprite(j).equals(sp)) {
	    				selector.setSelectedIndex(i);
	    				mainWindow.setVisible(true);
	    				return;
	    			}
	    		}
	    	}
			JOptionPane.showMessageDialog(mainWindow,
					"Could not open sprite group with specified sprite."
                    + "\nThe sprite is not currently being used in a set.",
                    "Sprite Not In A Set", JOptionPane.WARNING_MESSAGE);
		} else {
			throw new IllegalArgumentException();
		}
			
		mainWindow.setVisible(true);
	}

	public void hide() {
		if (isInited)
			mainWindow.setVisible(false);
	}
	
    private Image zoomImage(BufferedImage in, float zoom)
    {
        return in.getScaledInstance((int) (in.getWidth() * zoom), (int) (in
            .getHeight() * zoom), 0);
    }
	
	public void showInfo(int set) {
		width.setText(Integer.toString(sets[set].getWidth()));
		height.setText(Integer.toString(sets[set].getHeight()));
		palette.setSelectedIndex(sets[set].getPalette());
		numSpritesBox.setSelectedIndex(sets[set].getNumSprites());
		
		int i;
		for (i=0; i<sets[set].getNumSprites(); i++) {
			spriteFields[i].setEnabled(true);
			spriteFields[i].setText(sets[set].getSprite(i).getId());
			
			flipBoxes[i].setEnabled(true);
			flipBoxes[i].setSelected(sets[set].isFlipped(i));
		}
		
		for (; i<16; i++) {
			spriteFields[i].setEnabled(false);
			spriteFields[i].setText("");
			flipBoxes[i].setEnabled(false);
			flipBoxes[i].setSelected(false);
		}
	}
	
	public boolean preview() {
		int i;
		String key;
		boolean valid = true;
		for (i=0; i<numSpritesBox.getSelectedIndex(); i++) {
			key = spriteFields[i].getText();
			
			if (sprites.containsKey(key)) {
				Sprite sp = sprites.get(key);
				if ((sp.getWidth() != Integer.parseInt(width.getText()))
						|| (sp.getWidth() != Integer.parseInt(width.getText()))) {
					pics[i].setIcon(blankIcon);
		            pics[i].setEnabled(false);
		            spriteFields[i].setBackground(Color.yellow);
		            valid = false;
				} else {
					BufferedImage img = drawImage(sp.getImage(), pals[palette.getSelectedIndex()].getColorPalette());
					if (flipBoxes[i].isSelected()) {
					    AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
					    tx.translate(-img.getWidth(null), 0);
					    AffineTransformOp op = new AffineTransformOp(tx,
					        AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
					    img = op.filter(img, null);
					}
					pics[i].setIcon(new ImageIcon(zoomImage(img, 2)));
		            pics[i].setEnabled(true);
		            spriteFields[i].setBackground(Color.white);
				}
			} else {
				pics[i].setIcon(blankIcon);
	            pics[i].setEnabled(false);
	            spriteFields[i].setBackground(Color.red);
	            valid = false;
			}
		}
		
		for (; i<16; i++) {
			pics[i].setIcon(blankIcon);
            pics[i].setEnabled(false);
            spriteFields[i].setBackground(Color.white);
		}
		
		return valid;
	}
	
	public void applyChanges(int set) {
		if (preview()) {
			sets[set].setWidth(Integer.parseInt(width.getText()));
			sets[set].setHeight(Integer.parseInt(height.getText()));
			sets[set].setPalette(palette.getSelectedIndex());
			sets[set].setNumSprites(numSpritesBox.getSelectedIndex());
			
			for (int i=0; i<sets[set].numSprites; i++) {
				sets[set].setSprite(i, sprites.get(spriteFields[i].getText()), flipBoxes[i].isSelected());
			}
		} else {
			JOptionPane.showMessageDialog(mainWindow,
                    "Could not apply changes because some sprites were invalid.\n"
						+ "Please correct the sprites which have been highlighted in red.\n"
						+ "Please choose a correctly-sized sprite for any sprites which have been highlighted in yellow.",
                    "Could Not Apply Changes", JOptionPane.ERROR_MESSAGE);
		}
	}

	public void actionPerformed(ActionEvent e) {
		if (e.getSource().equals(selector)) {
			showInfo(selector.getSelectedIndex());
			preview();
		} else if (e.getSource().equals(preview)) {
			preview();
		} else if (e.getSource().equals(numSprites)) {
			int i;
			for (i=0; i<numSpritesBox.getSelectedIndex(); i++) {
				spriteFields[i].setEnabled(true);
				flipBoxes[i].setEnabled(true);
				pics[i].setEnabled(true);
			}
			for (; i<16; i++) {
				spriteFields[i].setEnabled(false);
				flipBoxes[i].setEnabled(false);
				pics[i].setEnabled(false);
			}
		} else if (e.getActionCommand().equals("apply")) {
        	applyChanges(selector.getSelectedIndex());
        } else if (e.getActionCommand().equals("close")) {
            hide();
        } else if (e.getActionCommand().startsWith("pics")) {
            Ebhack.main.showModule(SpriteEditor.class,
            		Integer.parseInt(spriteFields[Integer.parseInt(e.getActionCommand().substring(4), 16)].getText()));
        }
	}
}
